/*
 * license-updater - Copyright (c) 2012 MSF. All rights reserved.
 * 
 * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General
 * Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to
 * the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
 */
package br.msf.netbeans.licenseupdater;

import br.msf.commons.collections.LinkedProperties;
import br.msf.commons.exception.io.RuntimeIOException;
import br.msf.commons.lang.EnhancedStringBuilder;
import br.msf.commons.lang.MatchEntry;
import br.msf.commons.netbeans.util.FileObjectUtils;
import br.msf.commons.netbeans.util.FileType;
import br.msf.commons.util.CharSequenceUtils;
import br.msf.commons.util.CollectionUtils;
import br.msf.commons.util.IOUtils;
import br.msf.netbeans.licenseupdater.LicenseUpdaterParams.UpdateMode;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.netbeans.api.queries.FileEncodingQuery;
import org.openide.filesystems.FileAlreadyLockedException;
import org.openide.filesystems.FileLock;
import org.openide.filesystems.FileObject;

/**
 * <p>TODO
 *
 * @author Marcius da Silva da Fonseca (sf.marcius@gmail.com)
 * @version 1.0
 * @since license-updater-1.0
 */
public class LicenseController {

    protected static final String NB_LICENSE_TEMPLATE = "To change this template, choose Tools | Templates";
    protected static final Pattern JAVA_LICENSE = Pattern.compile("^\\s*/\\*(?:.|[\\n\\r])*?\\*/\\s*");
    protected static final String JAVA_LICENSE_START = "/*";
    protected static final String JAVA_LICENSE_LEADING = " * ";
    protected static final String JAVA_LICENSE_END = " */";
    protected LicenseUpdaterParams params;

    public LicenseController(LicenseUpdaterParams params) {
        this.params = params;
    }

    public LicenseController setParams(LicenseUpdaterParams params) {
        this.params = params;
        return this;
    }

    public LicenseController doTheTrick() {
        if (params.getProject() == null || params.getBasePackage() == null) {
            throw new IllegalStateException("Project or Base package not set!");
        }
        Collection<FileObject> files = FileObjectUtils.getFiles(params.getBasePackage().getFileObject(), true, FileType.JAVA_SRC);
        for (FileObject f : files) {
            updateJavaLicense(f);
        }
        return this;
    }

    private void updateJavaLicense(final FileObject fileObject) {
        assert fileObject != null && fileObject.isData() && FileType.isJava(fileObject.getExt());
        if (CharSequenceUtils.isBlank(params.getLicenseText())) {
            return;
        }
        try {
            EnhancedStringBuilder newSrc = getUpdatedSource(fileObject);
            if (newSrc != null) {
                if (!fileObject.canWrite()) {
                    throw new RuntimeIOException("Cant write to file: " + fileObject.getNameExt());
                }
                writeFile(fileObject, newSrc);
            }
        } catch (IOException ex) {
            throw new RuntimeIOException(ex);
        }
    }

    protected EnhancedStringBuilder getJavaLicense() {
        return formatLicense(params.getLicenseText(), JAVA_LICENSE_START, JAVA_LICENSE_END, JAVA_LICENSE_LEADING, params.getMaxLineLength()).
                replaceParams(getParams());
    }

    protected EnhancedStringBuilder formatLicense(final CharSequence text,
                                                  final String commentStart,
                                                  final String commentEnd,
                                                  final String leading,
                                                  final int maxLineLengh) {
        assert CharSequenceUtils.isNotBlank(text) && CharSequenceUtils.isNotBlank(commentStart) && CharSequenceUtils.isNotBlank(commentEnd);
        final EnhancedStringBuilder builder = new EnhancedStringBuilder(text).deletePattern("\r");
        final Collection<MatchEntry> newLines = builder.findPattern("\n");
        for (MatchEntry match : newLines) {
            if (match.getStart() > 0 && isPeriodFinalization(builder.charAt(match.getStart() - 1))
                && '\n' != builder.charAt(match.getStart() + 1)) {
                /* new lines not after a punctuation are removed */
                builder.replace(match, " ");
            }
        }
        builder.replacePattern(" +", " ");
        final Collection<String> lines = builder.split("\n");
        builder.clear().appendln(commentStart);
        int max = maxLineLengh - leading.length();
        for (CharSequence line : lines) {
            if (line.length() <= max) {
                // short line, just append the leading txt and the line itself
                builder.append(leading).appendln(line);
            } else {
                // long line, we need to break it down
                final List<String> subLines = breakdownLine(line.toString(), max);
                for (String subLine : subLines) {
                    builder.append(leading).appendln(subLine);
                }
            }
        }
        return builder.appendln(commentEnd);
    }

    protected EnhancedStringBuilder getUpdatedSource(final FileObject fileObject) throws IOException {
        final EnhancedStringBuilder builder = new EnhancedStringBuilder(fileObject.asText(FileEncodingQuery.getEncoding(fileObject).name()));
        final Collection<MatchEntry> currentLicense = builder.findPattern(JAVA_LICENSE);
        if (CollectionUtils.isNotEmpty(currentLicense)) {
            MatchEntry me = currentLicense.iterator().next();
            switch (params.getUpdateMode()) {
                case ALL_FILES:
                    /* source already have a license header. lets replace it */
                    builder.replace(me, getJavaLicense());
                    break;
                case WITHOUT_LICENSE_FILES:
                    /* we consider the default netbeans template as "file without license" */
                    if (builder.substring(me.getStart(), me.getEnd()).contains(NB_LICENSE_TEMPLATE)) {
                        builder.replace(me, getJavaLicense());
                    } else {
                        /* returning null sinalize that this FileObject will not be updated */
                        return null;
                    }
                    break;
            }
        } else {
            /* source dont have a license header. lets place it */
            builder.insert(0, getJavaLicense());
        }
        return builder;
    }

    protected void writeFile(final FileObject someFile, final CharSequence content) throws IOException {
        FileLock lock;
        try {
            lock = someFile.lock();
        } catch (FileAlreadyLockedException e) {
            return;
        }
        Writer to = null;
        try {
            to = new OutputStreamWriter(someFile.getOutputStream(lock), FileEncodingQuery.getEncoding(someFile));
            to.write(content.toString());
        } finally {
            IOUtils.closeQuietly(to);
            lock.releaseLock();
        }
    }

    protected List<String> breakdownLine(final String line, final int maxLenght) {
        assert line != null && maxLenght > 0;
        if (line.length() <= maxLenght || !CharSequenceUtils.containsPattern("\\s+", line)) {
            return Arrays.asList(line);
        }
        final EnhancedStringBuilder builder = new EnhancedStringBuilder(line);
        final List<String> lines = new ArrayList<String>();
        while (builder.length() > maxLenght) {
            int mark = -1;
            // here we look for the last ' ' occurrence before the MAX_LINE_LEN
            // or the first one if there is no ' ' before reaching the MAX_LINE_LEN limit
            for (int i = 0; i < builder.length(); i++) {
                if (builder.charAt(i) == ' ') {
                    if (i <= maxLenght || mark == -1) {
                        mark = i;
                    } else if (i > maxLenght) {
                        break;
                    }
                }
            }
            if (mark >= 0) {
                // if we found the ' ' occurrence we were looking for, we cut the line and create a subline with it
                lines.add(builder.substring(0, mark).trim());
                // remove the just added subline
                builder.delete(0, mark);
            } else {
                // it must be a super dupper mega word with no space! dont break it!
                lines.add(builder.trim().toString());
                builder.clear();
            }
        }
        if (builder.isNotBlank()) {
            // if there is some left-over on the buffer, we create the last subline with it.
            lines.add(builder.trim().toString());
        }
        return lines;
    }

    protected boolean isPeriodFinalization(final char c) {
        // .!? are period finalizers.
        // > can be a html tag limit, so we interpret it as a period finalizer too.
        return ".!?>".indexOf(c) >= 0;
    }

    protected Map<String, String> getParams() {
        Map<String, String> props = new LinkedHashMap<String, String>(LinkedProperties.getSystemProps());
        props.put("project.name", params.getProjectName());
        props.put("project.publisher", params.getProjectPublisher());
        return props;
    }
}
